//*********************************************************
//
//    Copyright (c) Microsoft. All rights reserved.
//    This code is licensed under the Apache License, Version 2.0.
//    THIS CODE IS PROVIDED *AS IS* WITHOUT WARRANTY OF
//    ANY KIND, EITHER EXPRESS OR IMPLIED, INCLUDING ANY
//    IMPLIED WARRANTIES OF FITNESS FOR A PARTICULAR
//    PURPOSE, MERCHANTABILITY, OR NON-INFRINGEMENT.
//
//*********************************************************

namespace Microsoft.Research.ScientificWorkflow.TridentComposer
{
    using System;
    using System.Collections.Generic;
    using System.Collections.ObjectModel;
    using System.Linq;
    using Microsoft.Research.ScientificWorkflow.TridentModel;
    using Microsoft.Research.ScientificWorkflow.TridentUtilities;
    using Microsoft.Research.DataLayer;
    using System.Globalization;

    /// <summary>
    /// Proxy class for scheduling and runing WF's.
    /// The clients using this class will need to have a connection to 
    /// the registry. The client schedules and runs only those WF's that
    /// are present in the registry.
    /// </summary>
    public class JobComposer
    {
        /// <summary>
        /// Connection object for the registry.
        /// </summary>
        private Connection registryHandle = null;

        /// <summary>
        /// Initializes a new instance of the job composer.
        /// </summary>
        /// <param name="registryConnection">Registry Connection.</param>
        public JobComposer(Connection registryConnection)
        {
            if (null == registryConnection || false == registryConnection.Alive)
            {
                TridentErrorHandler.ThrowTridentCustomException(TridentErrorConstants.ErrorNumber4000072,
                    TridentResourceManager.GetString("InvalidRegistryHandle"));
            }

            this.registryHandle = registryConnection;
        }


        /// <summary>
        /// Create a job in the registry. The executer picks this job and runs the workflow.
        /// </summary>
        /// <param name="jobDetails">The job details instance. Contains the details for the creation of the job.</param>
        /// <param name="node">The node on which the job is to be run.</param>
        /// <param name="user">The user who schedules the workflow. If null then the current user will be used.</param>
        /// <param name="isDebug">True if the job has to be run in Debug mode, False otherwise.</param>
        /// <returns>The new job created.</returns>
        public Job ExecuteWFOnMachine(JobDetailModel jobDetails, Machine node, User user, bool isDebug)
        {
            if (jobDetails == null)
            {
                throw new ArgumentNullException("jobDetails");
            }
            if (node == null)
            {
                throw new ArgumentNullException("node");
            }

            Job newJob = null;
            try
            {
                Activity workflowToExecute = Activity.Load(jobDetails.WorkflowId, this.registryHandle);

                ActivityInstance actInstance = ActivityInstance.Create(workflowToExecute, this.registryHandle);
                actInstance.Save();

                // Create a new Job instance for running the worfklow.
                string localJobName = !string.IsNullOrEmpty(jobDetails.JobName) ? jobDetails.JobName : string.Format(CultureInfo.CurrentCulture, "{0} - {1}", workflowToExecute.Name, DateTime.Now);

                newJob = Job.Create(localJobName,false, this.registryHandle);

                // If user is null then get the current user.
                if (null != user)
                {
                    newJob.ScheduledBy = user;
                }
                else
                {
                    newJob.ScheduledBy = this.GetCurrentUser();
                }
                newJob.Notes = jobDetails.Notes;
                newJob.IsTemplate = jobDetails.IsTemplate;
                newJob.IsDebug = jobDetails.IsDebug;
                newJob.Save();

                this.CreateJobRequiredParameters(actInstance, jobDetails.ParameterInfo);
                this.CreateAdditionalParameters(newJob, jobDetails.AdditionalParameters);

                newJob.Machines.Add(node);
                newJob.ActivityInstances.Add(actInstance);

                // Set this at the end. After all values are assigned.
                newJob.Status = JobStatus.Waiting;
                newJob.IsDebug = isDebug;
                newJob.Save();
                newJob.Refresh();
            }
            catch (TridentCustomException)
            {
                throw;
            }            
            catch (Exception ex)
            {
                TridentErrorHandler.ProcessKnownExceptionsInBusinessLogic(ex);
                TridentErrorHandler.HandleAndThrowBusinessLogicException(ex, TridentErrorConstants.ErrorNumber4000073,
                        TridentResourceManager.GetString("JobCreateError"));
            }
            return newJob;
        }

        /// <summary>
        /// Get the current user from the registry.
        /// </summary>
        /// <returns>The current user.</returns>
        private User GetCurrentUser()
        {
            try
            {
                string user = TridentAuthentication.LoggedUserInUserName; //Environment.UserName;
                return User.Load(user, this.registryHandle);
            }
            catch (RecordNotFoundException ex)
            {
                TridentErrorHandler.HandleAndThrowBusinessLogicException(ex, TridentErrorConstants.ErrorNumber4000074, 
                    TridentResourceManager.GetString("JobCreateError"));
            }
            catch (Exception ex)
            {
                TridentErrorHandler.HandleAndThrowBusinessLogicException(ex, TridentErrorConstants.ErrorNumber4000074,
                    TridentResourceManager.GetString("JobCreateError"));
            }
            return null;
        }

        /// <summary>
        /// Create the required job param entries for the workflow in the required params table.
        /// </summary>
        /// <param name="job">The job instance.</param>
        /// <param name="workflowParamInfo">The workflow info.</param>
        private void CreateJobRequiredParameters(ActivityInstance actInstance, WorkflowActivityParametersInfo workflowParamInfo)
        {
            try
            {
                // Load all assigned parameters.
                List<ParameterAssignment> activityParameterAssignments = new List<ParameterAssignment>();
                this.GetParameterAssignments(actInstance.Activity.BaseSequence, activityParameterAssignments);

                foreach (ActivityParameterInfo activityParameterInfo in workflowParamInfo.Parameters)
                {
                    // Get the activity unique name.
                    ParameterAssignment reqAssignment = activityParameterAssignments.FirstOrDefault(
                        p => p.Instance.Name.Equals(activityParameterInfo.ActivityUniqueName, StringComparison.Ordinal) &&
                        p.ActivityParameter.Name.Equals(activityParameterInfo.Name, StringComparison.Ordinal));

                    if (reqAssignment != null)
                    {
                        ActivityInstanceParameter reqParam = ActivityInstanceParameter.Create(
                            activityParameterInfo.Value,
                            actInstance,
                            reqAssignment,
                            this.registryHandle);

                        reqParam.Save();
                    }
                }
            }
            catch(Exception ex)
            {
                TridentErrorHandler.HandleAndThrowBusinessLogicException(ex, TridentErrorConstants.ErrorNumber4000075, 
                    TridentResourceManager.GetString("JobCreateError"));
            }
        }

        /// <summary>
        /// Create the additional parameters in the registry. 
        /// </summary>
        /// <param name="job">The job to which the additional parameters belong to.</param>
        /// <param name="additionalParameters">The collection of additional parameters.</param>
        private void CreateAdditionalParameters(Job job, Collection<AdditionalParametersModel> additionalParameters)
        {
            try
            {
                foreach (AdditionalParametersModel additionalParameter in additionalParameters)
                {
                    AdditionalParameter regAdditionalParameter = AdditionalParameter.Create(job, this.registryHandle);
                    regAdditionalParameter.Domain = additionalParameter.Domain;
                    regAdditionalParameter.ParamName = additionalParameter.Parameter;
                    regAdditionalParameter.ParamValue = additionalParameter.ParameterValue;
                    regAdditionalParameter.Save();
                }
            }
            catch (Exception ex)
            {
                TridentErrorHandler.HandleAndThrowBusinessLogicException(ex, TridentErrorConstants.ErrorNumber4000076,
                    TridentResourceManager.GetString("JobCreateError"));
            }
        }

        /// <summary>
        /// Gets the parameter assignments for the passed activity sequence.
        /// </summary>
        /// <param name="baseActivitySequence">The base activity sequence.</param>
        /// <param name="paramAssignmentList">The list in which the parameter assignments should be added.</param>
        private void GetParameterAssignments(ActivitySequence baseActivitySequence, List<ParameterAssignment> paramAssignmentList)
        {
            foreach (ParameterAssignment paramAssign in baseActivitySequence.ParameterAssignments)
            {
                if (paramAssign.ActivityParameter.Direction == DirectionType.Input && !paramAssign.IsBound)
                {
                    paramAssignmentList.Add(paramAssign);
                }
            }

            if (baseActivitySequence.Activity.IsComposite || baseActivitySequence.Activity.IsWorkflow)
            {
                foreach (ActivitySequence child in baseActivitySequence.Children)
                {
                    GetParameterAssignments(child, paramAssignmentList);
                }
            }
        }
    }
}
